#!/usr/bin/perl
#
# This tools parses /etc/tgt/targets.conf file and configures tgt
#
# Author:  Tomasz Chmielewski
# License: GPLv2
#

use strict;
use Config::General qw(ParseConfig);
use Getopt::Long;

# Our config file
my $configfile = "/etc/tgt/targets.conf";

sub usage {
	print <<EOF;
Usage:
tgt-admin [OPTION]...
This tool configures tgt targets.

  -e, --execute			read $configfile and execute tgtadm commands
      --delete <value>		delete all or selected targets
				(see "--delete help" for more info)
      --offline <value>		put all or selected targets in offline state
				(see "--offline help" for more info)
      --ready <value>		put all or selected targets in ready state
				(see "--ready help" for more info)
      --update <value>		update configuration for all or selected targets
				(see "--update help" for more info)
  -s, --show			show all the targets
  -C, --control-port <NNNN>	specify the control port to connect to
  -c, --conf <conf file>	specify an alternative configuration file
      --ignore-errors		continue even if tgtadm exits with non-zero code
  -f, --force			force some operations even if the target is in use
  -p, --pretend			only print tgtadm options
      --dump			dump current tgtd configuration (note: does not
				include detailed parameters, like write caching)
  -v, --verbose			increase verbosity (show tgtadm commands)
  -h, --help			show this help

EOF
	exit;
}

my %conf;
my $param = $ARGV[0];
my $execute = 0;
my $delete = 0;
my $offline = 0;
my $ready = 0;
my $update = 0;
my $show = 0;
my $alternate_conf="0";
my $ignore_errors = 0;
my $force = 0;
my $pretend = 0;
my $dump = 0;
my $verbose = 0;
my $help = 0;
my $control_port=0;
Getopt::Long::Configure("no_ignore_case");
my $result = GetOptions (
	"e|execute"     => \$execute,
	"delete=s"      => \$delete,
	"offline=s"     => \$offline,
	"ready=s"       => \$ready,
	"update=s"      => \$update,
	"s|show"        => \$show,
	"c|conf=s"      => \$alternate_conf,
	"ignore-errors" => \$ignore_errors,
	"f|force"       => \$force,
	"p|pretend"     => \$pretend,
	"dump"          => \$dump,
	"v|verbose"     => \$verbose,
	"h|help"        => \$help,
	"C|control-port=s"      => \$control_port,
);

if (($help == 1) || ($param eq undef)) {
	usage;
}

# Show all the targets and exit
if ($show == 1) {
	execute("tgtadm -C $control_port --op show --mode target");
	exit;
}

# Some variables/arrays/hashes we will use globally
my %tgtadm_output;
my %tgtadm_output_tid;
my %tgtadm_output_name;
my @largest_tid;
my $next_tid;
my %existing_accounts;

# Look up if iSNS is already on
sub check_isns {
	# We need to run as root
	if ( $> ) {
		die("You must be root to run this program.\n");
	}

	my @show_sys = `tgtadm -C $control_port --op show --mode sys`;
	foreach my $sys_line (@show_sys) {
		if ($sys_line =~ m/iSNS=On/) {
			return 1;
		}
	}
	# iSNS is not enabled if we're here
	return 0;
}

# Look up which targets are configured
sub process_targets {
	# We need to run as root
	if ( $> ) {
		die("You must be root to run this program.\n");
	}

	my @show_target = `tgtadm -C $control_port --op show --mode target`;
	my $tid;
	my $targetname;
	# Here, we create hashes of target names (all target data) and target tids
	foreach my $show_target_line (@show_target) {
		if ( $show_target_line =~ m/^Target (\d*): (.+)/ ) {
			$tid = $1;
			$targetname = $2;
			$tgtadm_output{$targetname} = $show_target_line;
			$tgtadm_output_tid{$targetname} = $tid;
			$tgtadm_output_name{$tid} = $targetname;
		} else {
			$tgtadm_output{$targetname} .= $show_target_line;
		}
	}
	# What is the largest tid?
	my @tids = values %tgtadm_output_tid;
	@largest_tid = sort { $a <=> $b } @tids;
	$next_tid = $largest_tid[$#largest_tid];
}

sub process_accounts {
	# We need to run as root
	if ( $> ) {
		die("You must be root to run this program.\n");
	}

	my @show_account = `tgtadm -C $control_port --op show --mode account`;
	# Here, we create a hash of accounts
	foreach my $show_account_line (@show_account) {
		if ( $show_account_line =~ m/^\s+(.*?)$/ ) {
			$existing_accounts{$1} = 1;
		}
	}
}

# Parse config file(s)
sub parse_configs {
	# Parse the config
	if ($alternate_conf ne 0) {
		# Check if alternative configuration file exists
		if (-e "$alternate_conf") {
			execute("# Using $alternate_conf as configuration file\n");
			%conf = ParseConfig(-ConfigFile => "$alternate_conf", -UseApacheInclude => 1, -IncludeDirectories => 1, -IncludeGlob => 1, -MergeDuplicateBlocks => 1);
		}
		else {
			die("Config file $alternate_conf not found. Exiting...\n");
		}
	} else {
		# Parse the config file with Config::General
		if (-e "$configfile") {
			%conf = ParseConfig(-ConfigFile => "$configfile", -UseApacheInclude => 1, -IncludeDirectories => 1, -IncludeGlob => 1, -MergeDuplicateBlocks => 1);
		} else {
			die("Config file $configfile not found. Exiting...\n");
		}
	}
}

# Add targets, if they are not configured already
my $default_driver;
my $target;
my $option;
my $value;
my $lun;

sub add_targets {
	my $single_target = $_[0];
	my $configured = $_[1];
	my $connected = $_[2];
	my $in_configfile = $_[3];
	my $isns_enabled = check_isns;
	foreach my $k (sort keys %conf) {

		if ($k eq "default-driver") {
			if (not length ref($conf{$k})) {
				$default_driver = $conf{$k};
			} else {
				print "Multiple default-driver definitions are not allowed!\n";
				print "Check your config file for errors.\n";
				exit 1;
			}
		} elsif ($k eq "ignore-errors") {
			if ($conf{$k} eq "yes") {
				$ignore_errors = 1;
			}
		} elsif (($k eq "iSNSAccessControl") || ($k eq "iSNSServerIP") ||
			($k eq "iSNSServerPort")) {
			if ($isns_enabled eq 0) {
				check_if_hash_array($conf{$k}, $k);
				execute("tgtadm -C $control_port --op update --mode sys --name $k -v $conf{$k}");
			}
		} elsif (($k eq "incomingdiscoveryuser") || ($k eq "outgoingdiscoveryuser")) {
			my @userpass = split(/ /, $conf{$k});
			check_value($userpass[1]);
			# Only delete or create account if it doesn't already exist
			if (! exists $existing_accounts{$userpass[0]} ) {
				execute("tgtadm -C $control_port --mode account --op new --user=$userpass[0] --password=$userpass[1]");
				$existing_accounts{$userpass[0]} = 1;
			}
			my $extra_str = "";
			if ($k eq "outgoingdiscoveryuser") {
				$extra_str = " --outgoing";
			}
			execute("tgtadm -C $control_port --mode account --op bind --user=$userpass[0] $extra_str");
		}

	}
	foreach my $k (sort keys %conf) {
		if ($k eq "iSNS") {
			if ($isns_enabled eq 0) {
				check_if_hash_array($conf{$k}, $k);
				execute("tgtadm -C $control_port --op update --mode sys --name $k -v $conf{$k}");
			} else {
				execute("# iSNS already enabled");
			}
		} elsif ($k eq "control-port") {
			if ($control_port eq 0) {
				$control_port = $conf{$k};
			} else {
				print "Multiple control-port definitions are not allowed!\n";
				print "Check your config file for errors.\n";
				exit 1;
			}
		}
	}
	# If $default_driver is empty, default to iscsi
	if (not defined $default_driver) {
		execute("# default-driver not defined, defaulting to iscsi.\n");
		$default_driver = "iscsi";
	}

	foreach my $k (sort keys %conf) {
		if ($k eq "target") {
			foreach my $k2 (sort keys %{$conf{$k}}) {
				# Do we run update or execute?
				if (length $single_target) {
					if ($single_target ne $k2) {
						next;
					} else {
						$target = $single_target;
					}
				} else {
					$target = $k2;
				}

				my $in_use = 0;
				if (length $single_target) {
					$in_use = main_delete($target);
				}
				my $allowall = 1;
				if ((not defined $tgtadm_output{$k2}) ||
					($update ne 0 && $in_use == 0) ||
					($update ne 0 && $in_use == 1 && $pretend == 1 && $force == 1))
					{
					# We have to find available tid
					if ($in_configfile == 1 && $configured == 0 && $pretend == 0) {
						my $maxtid = find_max_tid();
						$next_tid = $maxtid + 1;
					} elsif (length $single_target && $configured == 1) {
						$next_tid = $tgtadm_output_tid{$target};
					} else {
						$next_tid = $next_tid + 1;
					}

					# Before we add a target, we need to know its type
					# and other parameters which can be specified globally
					my %target_options;
					my $target_options_ref;
					my $data_key;
					foreach my $k3 (sort keys %{$conf{$k}{$k2}}) {
						$lun = 1;
						$option = $k3;
						$value = $conf{$k}{$k2}{$k3};
						check_value($value);
						$target_options{$option} = $value;
						$target_options_ref = \%target_options;
						$data_key = make_key($target_options_ref, "lun", "allow-in-use");
					}

					if (defined $target_options{"controller_tid"}) {
						$next_tid = $target_options{"controller_tid"};
					}
 
					if (not defined $target_options{"driver"}) {
						$target_options{"driver"} = $default_driver;
					}
					my $driver = $target_options{"driver"};
					execute("# Adding target: $target");
					execute("tgtadm -C $control_port --lld $driver --op new --mode target --tid $next_tid -T $target");
					foreach my $k3 (sort keys %{$conf{$k}{$k2}}) {
						$option = $k3;
						$value = $conf{$k}{$k2}{$k3};
						check_value($value);
						process_options($target_options_ref,$data_key);
						# If there was no option called "initiator-address", it means
						# we want to allow ALL initiators for this target
						if ($option eq "initiator-address") {
							$allowall = 0;
						}
					}

					if ($allowall == 1) {
						execute("tgtadm -C $control_port --lld $driver --op bind --mode target --tid $next_tid -I ALL");
					}

				} else {
					if (not length $configured || $in_use eq 1) {
						execute("# Target $target already exists!");
					}
				}
				$tgtadm_output_tid{$target} = $next_tid;
			}
			if (length $single_target && $in_configfile == 0 && $configured == 0) {
				print "Target $single_target is currently not configured\n";
				print "and does not exist in the config file - can't continue!\n";
				exit 1;
			}
			execute();
		}
	}
}

# Pre-parse the config and get some values we need
sub make_key {
	my $target_options_ref = shift;
	my @actions = @_;
	my %data_key;

	foreach my $action (@actions) {
		if (ref $$target_options_ref{'backing-store'} eq "HASH") {
			foreach my $testlun (keys %{$$target_options_ref{'backing-store'}}) {
				$data_key{$testlun}{$action} = $$target_options_ref{'backing-store'}{$testlun}{$action};
			}
		}
		if (ref $$target_options_ref{'direct-store'} eq "HASH") {
			foreach my $testlun (keys %{$$target_options_ref{'direct-store'}}) {
				$data_key{$testlun}{$action} = $$target_options_ref{'direct-store'}{$testlun}{$action};
			}
		}
	}
	return \%data_key;
}

# Some options can be specified only once
sub check_if_hash_array {
	my $check = $_[0];
	my $definition = $_[1];
	if (ref($check) eq 'ARRAY' || ref($check) eq "HASH") {
		if ($option) {
			print "Multiple '$definition' definitions in '$option' not allowed!\n";
			print "Check your config file for errors (target: $target).\n";
		} else {
			print "Multiple '$definition' definitions not allowed!\n";
			print "Check your config file for errors.\n";
		}
		exit 1;
	}
}

# Force an array if we just have one command
sub force_array {
	unless (ref($value) eq 'ARRAY') {
		$value = [ $value ];
	}
}

# If we start any external command, we want to know if it exists
sub check_exe {
	my $command = $_[0];
	my $option = $_[1];
	my @path = split(":", $ENV{PATH});
	my $exists = 0;
	foreach my $path (@path) {
		if ( -x "$path/$command" && -f "$path/$command" ) { $exists = 1 }
	}
	if ($exists == 0) {
		if ($command eq "sg_inq") {
			print "Command '$command' (needed by '$option') is not in your path - can't continue!\n";
			exit 1;
		} elsif ($command eq "lsof") {
			execute("# Command '$command' is not in your path.");
			execute("# Can't reliably check if device is not in use.");
			return 1;
		}
	}
}

# Apply additional parameters
sub add_params {
	my $param = shift;
	my $param_value = shift;
	my $lun = shift;
	my $driver = shift;

	if ($param eq "write-cache") {
		if ($param_value eq "off") {
			return("tgtadm -C $control_port --lld $driver --op update --mode logicalunit --tid $next_tid --lun=$lun --params mode_page=8:0:18:0x10:0:0xff:0xff:0:0:0xff:0xff:0xff:0xff:0x80:0x14:0:0:0:0:0:0");
		} elsif ($param_value eq "on" || not length $param_value) {
			return("# Write cache is enabled (default) for lun $lun.");
		} else {
			return("# WARNING! Unknown value ($param_value) to write-cache! Accepted values are \"on\" and \"off\".");
		}
	}

	if ($param eq "scsi_id" ||
	    $param eq "scsi_sn" ||
	    $param eq "vendor_id" ||
	    $param eq "product_id" ||
	    $param eq "product_rev" ||
	    $param eq "sense_format" ||
	    $param eq "removable" ||
	    $param eq "online" ||
	    $param eq "path" ||
	    $param eq "mode_page" ||
	    $param eq "lbppbe" ||
	    $param eq "la_lba" ||
	    $param eq "optimal_xfer_gran" ||
	    $param eq "optimal_xfer_len" ||
	    $param eq "readonly") {
		return("tgtadm -C $control_port --lld $driver --op update --mode logicalunit --tid $next_tid --lun=$lun --params $param=\"$param_value\"");
	}
	if ($param eq "params") {
		return("tgtadm -C $control_port --lld $driver --op update --mode logicalunit --tid $next_tid --lun=$lun --params \"$param_value\"");

	}
}

# Find next available LUN
sub find_next_lun {
	my $backing_store = $_[0];
	my $data_key_ref = $_[1];
	my $lun_collision = 0;
	my $lun_is_free = 0;
	my $found_lun = 1;
	while ($lun_is_free == 0) {
		foreach my $testlun (keys %$data_key_ref) {
			foreach my $testlun2 (values %{$$data_key_ref{$testlun}}) {
				if ($found_lun eq $testlun2) {
					$lun_collision = 1;
				}
			}
		}
		if ($lun_collision == 0) {
			$lun_is_free = 1;
		} else {
			$found_lun += 1;
		}
		$lun_collision = 0;
	}
	$$data_key_ref{$backing_store}{'lun'} = $found_lun;
	return $found_lun;
}

# Add backing or direct store
sub add_backing_direct {
	my $backing_store = $_[0];
	my $target_options_ref = $_[1];
	my $data_key_ref = $_[2];
	my $direct_store = $_[3];
	my $driver = $$target_options_ref{"driver"};
	my $bstype = $$target_options_ref{"bs-type"};

	# Is the device in use?
	my $can_alloc = 1;
	if ($bstype !~ "rbd" && $force != 1 && $$target_options_ref{'allow-in-use'} ne "yes") {
		$can_alloc = check_device($backing_store,$data_key_ref);
	}

	if ($can_alloc == 1 &&
	   ($bstype =~ "glfs" || $bstype =~ "rbd" || (-e $backing_store && ! -d $backing_store))) {
		my @exec_commands;
		my $device_type;
		my $bs_type;
		my $bsopts;
		my $bsoflags;
		my $block_size;
		my %luns;
		my @added_luns;
		# Find out LUNs which are "reserved" in the config file
		if (ref $value eq "HASH") {
			if (length $$data_key_ref{$backing_store}{'lun'}) {
				$lun = $$data_key_ref{$backing_store}{'lun'};
			} else {
				# Find an available lun if it wasn't specified
				$lun = find_next_lun($backing_store,$data_key_ref);
			}
		}
		# Process parameters for each lun / backing store
		if (ref $value eq "HASH") {
			my %params_added;
			my @mode_page;
			my @params;
			foreach my $store (keys %$value) {
				# lun
				if (length $$target_options_ref{"lun"}) {
					check_if_hash_array($$target_options_ref{"lun"}, "lun");
					$lun = $$target_options_ref{"lun"};
				}

				if (ref $$value{$store} eq "HASH" && $store eq $backing_store) {
					foreach my $store_option (keys %{$$value{$store}}) {
						my $result = $$value{$store}{$store_option};
						check_value($result);
						if ($store_option ne "mode_page" && $store_option ne "params")
							{ check_if_hash_array($result,$store_option) }
						# write-cache can be set globally per target and overridden per lun,
						# so we treat it differently
						if ($store_option ne "mode_page" && $store_option ne "write-cache"
							&& $store_option ne "params") {
							my $exec_command = add_params($store_option, $result, $lun, $driver);
							push(@exec_commands, $exec_command);
							$params_added{$store_option} = 1;
						}
						if ($store_option eq "write-cache") {
							my $exec_command = add_params($store_option, $result, $lun, $driver);
							$params_added{write_cache} = 1;
							push(@exec_commands, $exec_command);
						}
						if ($store_option eq "device-type") {
							$device_type = $result;
							$params_added{$store_option} = 1;
						}
						if ($store_option eq "bs-type") {
							$bs_type = $result;
							$params_added{$store_option} = 1;
						}
						if ($store_option eq "bsopts") {
							$bsopts = $result;
							$params_added{$store_option} = 1;
						}
						if ($store_option eq "bsoflags") {
							$bsoflags = $result;
							$params_added{$store_option} = 1;
						}
						if ($store_option eq "block-size") {
							$block_size = $result;
							$params_added{$store_option} = 1;
						}
						if ($store_option eq "mode_page") {
							unless (ref($result) eq 'ARRAY') {
								$result = [ $result ];
							}
							@mode_page = @$result;
							foreach my $mode_page (@mode_page) {
								my $exec_command = add_params($store_option, $mode_page, $lun, $driver);
								push(@exec_commands, $exec_command);
							}
						}
						if ($store_option eq "params") {
							unless (ref($result) eq 'ARRAY') {
								$result = [ $result ];
							}
							@params = @$result;
							foreach my $param (@params) {
								my $exec_command = add_params($store_option, $param, $lun, $driver);
								push(@exec_commands, $exec_command);
							}
						}
					}
				}
			}
			# Used only if lun is a direct-store
			my $sg_inq;
			my $sg_readcap;
			my %direct_params;
			if ($direct_store == 1) {
				$sg_inq=`sg_inq "$backing_store"`;
				if ($sg_inq=~m {
					Vendor\ identification:\s+?(.*?)\n
					\s+Product\ identification:\s+(.*?)\n
					\s+Product\ revision\ level:\s+(.*?)\n
					(?:\s+Unit\ serial\ number:\s+(.*?)\n)?
					}xs ) {
					# If they were not defined globally for a target,
					# add them now
					if (not length $$target_options_ref{vendor_id}) {
						$direct_params{vendor_id} = $1;
					}
					if (not length $$target_options_ref{product_id}) {
						$direct_params{product_id} = $2;
					}
					if (not length $$target_options_ref{product_rev}) {
						$direct_params{product_rev} = $3;
					}
					if (not length $$target_options_ref{scsi_sn}) {
						$direct_params{scsi_sn} = $4;
					}
				}

				$sg_inq=`sg_inq -p 0xb0 "$backing_store"`;
				if ($sg_inq=~m {
					Optimal\ transfer\ length\ granularity:\s+?(\d+).*?\n
					\s+Maximum\ transfer\ length:\s+?(\d+).*?\n
					\s+Optimal\ transfer\ length:\s+?(\d+).*?\n
					}xs ) {
					if (not length $$target_options_ref{optimal_xfer_gran}) {
						$direct_params{optimal_xfer_gran} = $1;
					}
					# $2 unused
					if (not length $$target_options_ref{optimal_xfer_len}) {
						$direct_params{optimal_xfer_len} = $3;
					}
				}

				$sg_readcap=`sg_readcap -16 "$backing_store"`;
				if ($sg_readcap=~ m {
					Logical\ block\ length=(\d+).*?\n
					\s+Logical\ blocks\ per\ physical\ block\ exponent=(\d+).*?\n
					\s+Lowest\ aligned\ logical\ block\ address=(\d+).*?\n
					}xs) {
					if (not length $block_size) {
						$block_size = $1;
					}
					if (not length $$target_options_ref{lbppbe}) {
						$direct_params{lbppbe} = $2;
					}
					if (not length $$target_options_ref{la_lba}) {
						$direct_params{la_lba} = $3;
					}
				}
			}

			# Add these parameters if they were not overwritten in the config file
			my @opts = ("scsi_id", "sense_format", "removable", "online", "path", "readonly");
			foreach my $single_opt (@opts) {
				check_if_hash_array($$target_options_ref{$single_opt},$single_opt);
				if ($params_added{$single_opt} ne 1 && length $$target_options_ref{$single_opt}) {
					my $exec_command = add_params($single_opt, $$target_options_ref{$single_opt}, $lun, $driver);
					push(@exec_commands, $exec_command);
					$params_added{$single_opt} = 1;
				}
			}
			# These options can be fetched by sg_inq for direct-store
			my @opts = ("vendor_id", "product_id", "product_rev", "scsi_sn",
				    "lbppbe", "la_lba", "optimal_xfer_gran", "optimal_xfer_len");
			foreach my $single_opt (@opts) {
				check_if_hash_array($$target_options_ref{$single_opt},$single_opt);
				my $this_opt;
				if (length $$target_options_ref{$single_opt}) {
					$this_opt = $$target_options_ref{$single_opt};
				} elsif (length $direct_params{$single_opt}) {
					$this_opt = $direct_params{$single_opt};
				}
				if ($params_added{$single_opt} ne 1 && length $this_opt) {
					my $exec_command = add_params($single_opt, $this_opt, $lun, $driver);
					push(@exec_commands, $exec_command);
					$params_added{$single_opt} = 1;
				}
			}
			# write-cache
			if ($params_added{write_cache} ne 1) {
				my $exec_command = add_params("write-cache", $$target_options_ref{"write-cache"}, $lun, $driver);
				push(@exec_commands, $exec_command);
				$params_added{write_cache} = 1;
			}
			# mode_page
			unless (ref($$target_options_ref{mode_page}) eq 'ARRAY') {
				$$target_options_ref{mode_page} = [ $$target_options_ref{mode_page} ];
			}
			foreach my $mode_page (@{$$target_options_ref{"mode_page"}}) {
				if (length $mode_page) {
					my $exec_command = add_params("mode_page", $mode_page, $lun, $driver);
					push(@exec_commands, $exec_command);
				}
			}
			# params
			unless (ref($$target_options_ref{params}) eq 'ARRAY') {
				$$target_options_ref{params} = [ $$target_options_ref{params} ];
			}
			foreach my $param (@{$$target_options_ref{"params"}}) {
				if (length $param) {
					my $exec_command = add_params("params", $param, $lun, $driver);
					push(@exec_commands, $exec_command);
				}
			}
			# device-type
			if ($params_added{"device-type"} ne 1) {
				check_if_hash_array($$target_options_ref{"device-type"}, "device-type");
				$device_type = $$target_options_ref{"device-type"};
			}
			# bs-type
			if ($params_added{"bs-type"} ne 1) {
				check_if_hash_array($$target_options_ref{"bs-type"}, "bs-type");
				$bs_type = $$target_options_ref{"bs-type"};
			}
			# bsopts
			if ($params_added{"bsopts"} ne 1) {
				check_if_hash_array($$target_options_ref{"bsopts"}, "bsopts");
				$bsopts = $$target_options_ref{"bsopts"};
			}
			# bsoflags
			if ($params_added{"bsoflags"} ne 1) {
				check_if_hash_array($$target_options_ref{"bsoflags"}, "bsoflags");
				$bsoflags = $$target_options_ref{"bsoflags"};
			}
		} else {
			print "If you got here, this means your config file is not supported.\n";
			print "Please report it to stgt mailing list and attach your config files.\n";
			exit 1;
		}
		# Execute commands for a given LUN
		if (length $device_type) { $device_type = "--device-type $device_type" };
		if (length $bs_type) { $bs_type = "--bstype $bs_type" };
		if (length $bsopts) { $bsopts = "--bsopts $bsopts" };
		if (length $bsoflags) { $bsoflags = "--bsoflags $bsoflags" };
		if (length $block_size) { $block_size = "--blocksize $block_size" };
		execute("tgtadm -C $control_port --lld $driver --op new --mode logicalunit --tid $next_tid --lun $lun -b \"$backing_store\" $device_type $bs_type $bsopts $bsoflags $block_size");

		# Commands should be executed in order
		my @execute_last;
		foreach my $exec_command (@exec_commands) {
			if (length $exec_command) {
				if ($exec_command =~ m/params mode_page/) {
					execute($exec_command);
				} else {
					push(@execute_last, $exec_command);
				}
			}
		}
		foreach my $exec_command (@execute_last) {
			execute($exec_command);
		}
		$lun += 1;
		return $lun;
	} elsif ($can_alloc == 0) {
		execute("# Skipping device $backing_store - it is in use.");
		execute("# You can override it with --force or 'allow-in-use yes' config option.");
		execute("# Note - do so only if you know what you're doing, you may damage your data.");
	} else {
		execute("# Skipping device: $backing_store");
		execute("# $backing_store does not exist - please check the configuration file");
	}
}

# Process options from the config file
sub process_options {
	my $target_options_ref = $_[0];
	my $data_key_ref = $_[1];
	my $driver = $$target_options_ref{"driver"};
	if ($option eq "backing-store" || $option eq "direct-store") {
		my $direct_store = 0;
		if ($option eq "direct-store") {
			check_exe("sg_inq", "option direct-store");
			$direct_store = 1;
		}

		# We want to make everything a hash to use it
		# in the same way later on
		unless (ref($value)) {
			$value = { $value }
		}

		my %arrvalue;
		if (ref($value) eq "ARRAY") {
			foreach my $backing_store (@$value) {
				$arrvalue{$backing_store} = 1;
			}
			$value = \%arrvalue;
		}

		if (ref($value) eq "HASH") {
			foreach my $backing_store (sort keys %$value) {
				if ($backing_store =~ m/HASH/) {
					print "\nYour config file is not supported. See targets.conf.example for details.\n";
					exit 1;
				}
			}
			foreach my $backing_store (sort keys %$value) {
				add_backing_direct($backing_store,$target_options_ref,$data_key_ref,$direct_store);
			}
		}
	}

	if ($option eq "incominguser") {
	        # if we have one command, force it to be an array anyway
		force_array();
		my @value_arr = @$value;
		foreach my $incominguser (@value_arr) {
			my @userpass = split(/ /, $incominguser);
			check_value($userpass[1]);
			# Only delete or create account if it doesn't already exist
			if (! exists $existing_accounts{$userpass[0]} ) {
				execute("tgtadm -C $control_port --lld $driver --mode account --op new --user=$userpass[0] --password=$userpass[1]");
				$existing_accounts{$userpass[0]} = 1;
			}
			execute("tgtadm -C $control_port --lld $driver --mode account --op bind --tid=$next_tid --user=$userpass[0]");
		}
	}

	if ($option eq "outgoinguser") {
	        # if we have one command, force it to be an array anyway
		force_array();
		if (length @$value[1]) {
			execute("# Warning: only one $option is allowed. Will only use the first one.");
		}
		my @userpass = split(/ /, @$value[0]);
		check_value($userpass[1]);
		# Only delete or create account if it doesn't already exist
		if (! exists $existing_accounts{$userpass[0]} ) {
			execute("tgtadm -C $control_port --lld $driver --mode account --op new --user=$userpass[0] --password=$userpass[1]");
			$existing_accounts{$userpass[0]} = 1;
		}
		execute("tgtadm -C $control_port --lld $driver --mode account --op bind --tid=$next_tid --user=$userpass[0] --outgoing");
	}

	if ($option eq "initiator-address") {
	        # if we have one command, force it to be an array anyway
		force_array();
		my @value_arr = @$value;
		foreach my $initiator_address (@value_arr) {
			check_value($initiator_address);
			execute("tgtadm -C $control_port --lld $driver --op bind --mode target --tid $next_tid -I $initiator_address");
		}
	}

	if ($option eq "initiator-name") {
	        # if we have one command, force it to be an array anyway
		force_array();
		my @value_arr = @$value;
		foreach my $initiator_name (@value_arr) {
			check_value($initiator_name);
			execute("tgtadm -C $control_port --lld $driver --op bind --mode target --tid $next_tid -Q $initiator_name");
		}
	}

	if ($option =~ m/^MaxRecvDataSegmentLength$|
			^MaxXmitDataSegmentLength$|
			^HeaderDigest$|
			^DataDigest$|
			^InitialR2T$|
			^MaxOutstandingR2T$|
			^ImmediateData$|
			^FirstBurstLength$|
			^MaxBurstLength$|
			^DataPDUInOrder$|
			^DataSequenceInOrder$|
			^ErrorRecoveryLevel$|
			^IFMarker$|
			^OFMarker$|
			^DefaultTime2Wait$|
			^DefaultTime2Retain$|
			^OFMarkInt$|
			^IFMarkInt$|
			^MaxConnections$|
			^state/x) {
	        # if we have one command, force it to be an array anyway
		force_array();
		if (length @$value[1]) {
			execute("# Warning: only one $option is allowed. Will only use the first one.");
		}
		check_value($option);
		execute("tgtadm -C $control_port --lld $driver --mode target --op update --tid $next_tid --name $option --value $$value[0]");
	}

}

# If the target is configured, but not present in the config file,
# try to remove it
sub remove_targets {

	process_targets;
	my @all_targets = keys %tgtadm_output_tid;

	foreach my $existing_target (@all_targets) {
		my $dontremove = 0;
		my $k2;
		foreach my $k (sort keys %conf) {
			if ( $k eq "target" ) {
				foreach $k2 (sort keys %{$conf{$k}}) {
					if ( $k2 eq $existing_target ) {
						$dontremove = 1;
					}
				}

				if ( $dontremove == 0 ) {
					# Remove the target
					main_delete($existing_target);
				}
			}
		}
	}
}

# Dump current tgtd configuration
sub dump_config {

	process_targets;

	my @all_targets = keys %tgtadm_output_tid;

	# If all targets use the same driver, use it only once in the config
	my $skip_driver = 0;
	my @drivers_combined;
	foreach my $current_target (@all_targets) {
		my $driver = show_target_info($current_target, "driver");
		push (@drivers_combined, $driver);
	}

	my %drivers_uniq;
	@drivers_uniq{@drivers_combined} = ();
	my @drivers_combined_uniq = sort keys %drivers_uniq;

	if (scalar @drivers_combined_uniq == 1) {
		print "default-driver $drivers_combined_uniq[0]\n\n";
	}

	# Print everything else in the config
	foreach my $current_target (@all_targets) {
		my $target_name = show_target_info($current_target, "target_name");
		print "<target $target_name>\n";

		if (scalar @drivers_combined_uniq gt 1) {
			my $driver = show_target_info($current_target, "driver");
			print "\tdriver $driver\n";
		}

		my @backing_stores = show_target_info($current_target, "backing_stores");
		foreach my $backing_store (@backing_stores) {
			print "\tbacking-store $backing_store\n";
		}

		my @account_information = show_target_info($current_target, "account_information");
		foreach my $account (@account_information) {
			if ($account =~ /(.+)\ \(outgoing\)/) {
				print "\toutgoinguser $1 PLEASE_CORRECT_THE_PASSWORD\n";
			} elsif (length $account) {
				print "\tincominguser $account PLEASE_CORRECT_THE_PASSWORD\n";
			}
		}

		my @acl_information = show_target_info($current_target, "acl_information");
		if (scalar(@acl_information) != 1 || $acl_information[0] ne "ALL") {
			foreach my $ini_address (@acl_information) {
				# determine here if acl is an IP or IQN
				if ( $ini_address =~ /^[0-9,.E]+$/ ) {
					print "\tinitiator-address $ini_address\n";
				} else {
					print "\tinitiator-name $ini_address\n";
				}
			}
		}
		print "</target>\n\n";
	}
}

# Offline or ready targets
sub ready_offline_targets {
	my $var = $_[0]; # This variable is either "offline" or "ready"
	my $off_ready;
	if ($ready eq 0) {
		$off_ready = $offline
	} elsif ($offline eq 0) {
		$off_ready = $ready
	} else {
		print "Invalid value (you can't use both ready and offline)!\n";
		exit 1;
	}
	if ($off_ready eq "help") {
		print <<EOF;
      --$var <value>		$var all or selected targets

Example usage:
      --$var help	      - display this help
      --$var ALL	      - $var all targets
      --$var tid=4	      - $var target 4 (target with tid 4)
      --$var iqn.2008-08.com.example:some.target - $var this target

EOF
	} elsif ($off_ready eq "ALL") {
		process_targets;
		# Run over all targets and offline/ready them
		my @all_targets = keys %tgtadm_output_tid;
		foreach my $existing_target (@all_targets) {
			execute("tgtadm -C $control_port --op update --mode target --tid=$tgtadm_output_tid{$existing_target} -n state -v $var");
		}
	} elsif ($off_ready =~ m/tid=(.+)/) {
		process_targets;
		execute("tgtadm -C $control_port --op update --mode target --tid=$1 -n state -v $var");
	} else {
		process_targets;
		if (length $tgtadm_output_tid{$off_ready}) {
			execute("tgtadm -C $control_port --op update --mode target --tid=$tgtadm_output_tid{$off_ready} --name=\"$off_ready\" -n state -v $var");
		} else {
			print "There is no target with name \"$off_ready\", can't $var it!\n";
			exit 1;
		}
	}
}

# Show info for a given target
sub show_target_info {
	my $existing_target = $_[0];
	my $task = $_[1];
	# Returns target information
	if ($task eq "target_name") {
		if ($tgtadm_output{$existing_target} =~ m/^Target (\d*): (.+)/ ) {
			return $2;
		}
	# Returns driver information
	} elsif ($task eq "driver") {
		if ($tgtadm_output{$existing_target} =~ m/\s+Driver: (.+)/ ) {
			return $1;
		}
	# Returns backing store
	} elsif ($task eq "backing_stores") {
		if ($tgtadm_output{$existing_target} =~ m/\s+Backing store path: (?!None)(.+)/ ) {
			my @backing_stores = $tgtadm_output{$existing_target} =~ m{\s+Backing store path: (?!None\n)(.+)}g;
			return @backing_stores;
		}
		return;
	# Returns account information:
	} elsif ($task eq "account_information") {
		if ($tgtadm_output{$existing_target} =~ m{
			\s+Account\ information:\n(.*)\n\s+ACL\ information:
			     }xs
		      ) {
			my @accounts = split(/\n/, $1);
			my @account_information;
			foreach my $user (@accounts) {
				my @var = split(/^\s+/, $user);
				push(@account_information, $var[1]);
			}
			return @account_information;
		}
	# Returns ACL information
	} elsif ($task eq "acl_information") {
		if ($tgtadm_output{$existing_target} =~ m{
			\s+ACL\ information:\n(.*)
				}xs
			) {
			my @ini_addresses = split(/\n/, $1);
			my @acls;
			foreach my $ini_address (@ini_addresses) {
				my @var = split(/^\s+/, $ini_address);
				push(@acls, $var[1]);
			}
			return @acls;
		}
	# Returns sessions
	} elsif ($task eq "sessions") {
		my %sessions;
		if ($tgtadm_output{$existing_target} =~ m{
			\s+I_T\ nexus\ information:\n(.*)LUN\ information:
				}xs
			) {
			my @var = split(/\n/, $1);
			my $sid;
			my $cid;

			foreach my $line (@var) {
			if ($line =~ m/\s+I_T nexus:\ (.+)/) {
					$sid = $1;
				} else {
					if ($line =~ m/\s+Connection:\ (.+)/) {
						$cid = $1;
						$sessions{$sid} = $cid;
					}
				}
			}
		}
		return %sessions;
	}
}

# Main subroutine for deleting targets
sub main_delete {
	my $current_target = $_[0];
	my $current_tid = $_[1];
	my $configured = check_configured($current_target);
	my $del_upd_text;
	# Check if the target has initiators connected
	if ($tgtadm_output{$current_target} =~ m/\s+Connection:/) {
		if ($force == 1) {
			execute("# Removing target: $current_target");
			# Remove ACLs first
			my @acl_info = show_target_info($current_target, "acl_information");
			foreach my $acl (@acl_info) {
				execute("tgtadm -C $control_port --op unbind --mode target --tid $tgtadm_output_tid{$current_target} -I $acl");
			}
			# Now, remove all sessions / connections from that tid
			my %sessions = show_target_info($current_target, "sessions");
			foreach my $sid (keys %sessions) {
				foreach my $cid ($sessions{$sid}) {
					execute("tgtadm -C $control_port --op delete --mode conn --tid $tgtadm_output_tid{$current_target} --sid $sid --cid $cid");
				}
			}
			execute("tgtadm -C $control_port --mode target --op delete --tid=$tgtadm_output_tid{$current_target}");
		} else {
			if ($update ne 0) {
				$del_upd_text = "updated";
			} else {
				$del_upd_text = "deleted";
			}
			execute("# Target with tid $tgtadm_output_tid{$current_target} ($current_target) is in use, it won't be $del_upd_text.");
			return 1;
		}
	} elsif (length $tgtadm_output_tid{$current_target}) {
		execute("# Removing target: $current_target");
		execute("tgtadm -C $control_port --mode target --op delete --tid=$tgtadm_output_tid{$current_target}");
	} else {
		if (length $current_tid) {
			execute("# Target with tid $current_tid does not exist!");
		} else {
			execute("# Target with name $current_target does not exist!");
		}
	}
	if ($configured ne 0) {
		execute();
	}
	return 0;
}

# Delete the targets
sub delete_targets {

	if ($delete eq "help") {
		print <<EOF;
      --delete <value>		delete all or selected targets
				The target will be deleted only if it's not used
				(no initiator is connected to it).
				If you want to delete targets which are in use,
				you have to add "--force" flag.

Example usage:
      --delete help	      - display this help
      --delete ALL	      - delete all targets
      --delete tid=4	      - delete target 4 (target with tid 4)
      --delete iqn.2008-08.com.example:some.target - delete this target

EOF
		exit;
	} elsif ($delete eq "ALL") {
		process_targets;
		# Run over all targets and delete them
		my @all_targets = keys %tgtadm_output_tid;
		foreach my $current_target (@all_targets) {
			main_delete($current_target);
		}
	} elsif ($delete =~ m/^tid=(.+)/) {
		# Delete by tid
		process_targets;
		my $current_target = $tgtadm_output_name{$1};
		main_delete($current_target, $1);
	} else {
		# Delete by name
		process_targets;
		my $current_target = $delete;
		main_delete($current_target);
	}
}

# Update targets
sub update_targets {
	if ($update eq "help") {
		print <<EOF;
      --update <value>		update all or selected targets
				The target will be updated only if it's not used
				(no initiator is connected to it).
				If you want to update targets which are in use,
				you have to add "--force" flag.

Example usage:
      --update help	      - display this help
      --update ALL	      - update all targets
      --update tid=4	      - update target 4 (target with tid 4)
      --update iqn.2008-08.com.example:some.target - update this target

EOF
		exit;
	} elsif ($update eq "ALL") {
		# Run over all targets and delete them if they are not in use
		parse_configs;
		process_targets;
		my @targets_combined = combine_targets();
		foreach my $current_target (@targets_combined) {
			my $configured = check_configured($current_target);
			my $connected = check_connected($current_target);
			my $in_configfile = check_in_configfile($current_target);
			combine_targets();
			if (($in_configfile == 0) && ($configured == 1)) {
				# Delete the target if it's not in the config file
				main_delete($current_target);
			} else {
				add_targets($current_target, $configured, $connected, $in_configfile);
			}

		}
	} elsif ($update =~ m/^tid=(.+)/) {
		# Update by tid
		parse_configs;
		process_targets;
		my $current_target = $tgtadm_output_name{$1};
		my $configured = check_configured($current_target);
		my $connected = check_connected($current_target);
		my $in_configfile = check_in_configfile($current_target);
		if (($in_configfile == 0) && ($configured == 1)) {
			# Delete the target if it's not in the config file
			main_delete($current_target);
		} elsif ($configured == 1) {
			add_targets($current_target, $configured, $connected, $in_configfile);
		} else {
			print "There is no target with tid $1, can't continue!\n";
			exit 1;
		}
	} else {
		# Update by name
		parse_configs;
		process_targets;
		my $current_target = $update;
		my $configured = check_configured($current_target);
		my $connected = check_connected($current_target);
		my $in_configfile = check_in_configfile($current_target);
		if ($in_configfile == 0 && $configured == 1) {
			# Delete the target if it's not in the config file
			main_delete($current_target);
		} else {
			add_targets($current_target, $configured, $connected, $in_configfile);
		}
	}
}

# Find the biggest tid
sub find_max_tid {
	my @all_targets = keys %tgtadm_output_tid;
	my $maxtid = 0;
	foreach my $var (@all_targets) {
		if ($tgtadm_output_tid{$var} > $maxtid) {
			$maxtid = $tgtadm_output_tid{$var};
		}
	}
	return $maxtid;
}

# Combine targets from the config file and currently configured targets
sub combine_targets {
	my @targets_in_configfile;
	my @all_targets = keys %tgtadm_output_tid;
	my @targets_combined;
	# Make an array of targets in the config file
	foreach my $k (sort keys %conf) {
		if ( $k eq "target" ) {
			foreach my $k2 (sort keys %{$conf{$k}}) {
				push(@targets_in_configfile, $k2)
			}
		}
	}
	# Use only unique elements from both arrays
	foreach my $current_target (@all_targets) {
		push (@targets_combined, $current_target) unless grep { $_ eq $current_target } @targets_in_configfile;
	}
	@targets_combined = (@targets_combined, @targets_in_configfile);
	return @targets_combined;
}

# Check if a value is correct
sub check_value {
	if ( not defined $_[0] or not length $_[0] ) {
		print "\nOption $option has a missing value!\n";
		print "Check your config file for errors (target: $target)\n";
		exit 1;
	}
}

# Check if the target is in the config file
sub check_in_configfile {
	my $current_target = $_[0];
	my $result;
	foreach my $k (sort keys %conf) {
		if ( $k eq "target" ) {
			foreach my $k2 (sort keys %{$conf{$k}}) {
				if ($k2 eq $current_target) {
					return 1;
				}
			}
			# If we're here, we didn't find a match
			return 0;
		}
	}
}

# Check if the target is configured in tgtd
sub check_configured {
	my $current_target = $_[0];
	if (length $tgtadm_output_tid{$current_target}) {
		return 1;
	} else {
		return 0;
	}
}

# Check if any initiators are connected to the target
sub check_connected {
	my $current_target = $_[0];
	if ($tgtadm_output{$current_target} =~ m/\s+Connection:/) {
		return 1;
	} else {
		return 0;
	}
}

# Check if a device can be allocated
# Device can be used "by system" (i.e. mounted, used as swap, as a part of
# a RAID array etc.) or "by user" - i.e., already by tgtd, or someone doing:
#    dd if=/dev/1st_device of=/dev/2nd_device
# We shouldn't allow a device to be used more than one time, as it could
# cause corruption when written several times. Unless the user really wants to.
sub check_device {
	my $backing_store = $_[0];
	my $data_key_ref = $_[1];

	# If allow-in-use is "yes", there is no need to do
	# farther tests
	if ($$data_key_ref{$backing_store}{'allow-in-use'} eq "yes") {
		return 1;
	}

	if (! -e $backing_store) {
		return 1;
	}

	# Check if the system uses this device
	use Fcntl qw(O_RDONLY O_EXCL);
	use Errno;
	sysopen(FH, $backing_store, O_RDONLY | O_EXCL);
	if ($!{EBUSY}) {
		execute("# Device $backing_store is used by the system (mounted, used by swap?).");
		return 0;
	}
	close(FH);

	# Check if userspace uses this device
	my $lsof_check = check_exe("lsof");
	if ($lsof_check ne 1) {
		system("lsof \"$backing_store\" >/dev/null 2>&1");
		my $exit_value  = $? >> 8;
		if ($exit_value eq 0) {
			execute("# Device $backing_store is used (already tgtd target?).");
			execute("# Run 'lsof $backing_store' to see the details.");
			return 0;
		}
	}
	return 1;
}

# Execute or just print (or both) everything we start or would start
sub execute {
	if ($pretend == 0) {

		my $args = "@_";
		if ($verbose == 1) {
			print "$args\n";
		}
		# Don't try to execute if it's a comment
		my @execargs = split(/#/, $args);
		if ($execargs[0] ne undef) {
			system($args);

			# If non-zero exit code was return, exit
			my $exit_value  = $? >> 8;
			if (($exit_value != 0) && ($ignore_errors == 0)) {
				print "Command:\n\t$args\nexited with code: $exit_value.\n";
				exit $exit_value;
			}
		}

	} elsif ($pretend == 1) {
		print "@_\n";
	}
}

if ($execute == 1) {
	parse_configs;
	process_targets;
	process_accounts;
	add_targets;
	remove_targets;
} elsif ($delete ne 0) {
	delete_targets;
} elsif ($update ne 0) {
	process_accounts;
	update_targets;
} elsif ($dump == 1) {
	dump_config;
} elsif ($offline ne 0) {
	ready_offline_targets("offline");
} elsif ($ready ne 0) {
	ready_offline_targets("ready");
} else {
	print "No action specified.\n";
}

